import java.util.ArrayList;

public abstract class Node {

	protected double input; //Input of node or sum of incoming connections
	protected double output; //Result of activation func on input
	
	protected ArrayList<Connection> forwardConnections;//Node connections
	protected double delta; //Error term

	protected final double[] coords = new double[2];//Coordinates in layer; used by CPPN.

  double incr = 0.0;
	
	public Node(double[] c) {
		coords[0] = -1 + (c[0]*0.2);
		coords[1] = -1 + (c[1]*0.2);
		//coords = c;
		input = 0;
		output = 0;
		delta = 0;
		forwardConnections = new ArrayList<Connection>();
	}
	
	//These abstract functions define the type of node
	//Concrete implementations are in private classes in NeuralNet.java
	abstract double activationFunc(double x);	
	abstract double activationDeriv(double x);
	abstract String getType();
	
	public void forwardPass() {
		output = activationFunc(input);
		for (int i = 0; i < forwardConnections.size(); i++) {
			forwardConnections.get(i).node.addInput(forwardConnections.get(i).weight * output);
		}
	}
	
	//Computes output-node delta and returns squared error
	public double computeError(double target) {
    delta = (target - output)*(activationDeriv(input) + incr);
    return Math.pow((target - output),2)/2;
		//delta = (target - output);
		//return -(target*Math.log(output)+(1-target)*Math.log(1-output));
	}
	
	public void propagateError() {
		delta = 0;
		for (int i = 0; i < forwardConnections.size(); i++) {
			//Compute delta based on delta of forward connections
			delta += forwardConnections.get(i).node.delta * forwardConnections.get(i).weight;
			//Weight diffs are summed over multiple training examples
			forwardConnections.get(i).weightDiff += output*forwardConnections.get(i).node.delta;
			forwardConnections.get(i).dEdw += output*forwardConnections.get(i).node.delta;
		}
		//Multiply summed delta with activation derivative
		delta *= activationDeriv(input) + incr;
	}
	
	public void updateWeights(double learnRate, double momentum, boolean corr) {
		for (int i = 0; i < forwardConnections.size(); i++) {
      //Only update connections to output nodes when using correlation
      if (!corr || forwardConnections.get(i).node.size() == 0) {
			  forwardConnections.get(i).updateWeight(learnRate, momentum);
      } else {
        forwardConnections.get(i).weightDiff = 0;
        forwardConnections.get(i).dEdw = 0;
      }
		}
	}


	public void resetMomentums() {
		for (int i = 0; i < forwardConnections.size(); i++) {
      forwardConnections.get(i).prevDiff = 0;
    }
  }

	public void resetDelta() {
		delta = 0;
	}
	
	public double getWeight(int i) {
		return forwardConnections.get(i).weight;
	}
	
	public void setWeight(int i, double weight) {
		forwardConnections.get(i).weight = weight;
	}
	
	public void addConnection(Node n) {
		forwardConnections.add(new Connection(n));
	}
	
	public int size() {
		return forwardConnections.size();
	}
	
	public Node getConnection(int i) {
		return forwardConnections.get(i).node;
	}
	
	public void addInput(double i) {
		input += i;
	}

  public double getInput() {
    return input;
  }
	
	public void resetInput() {
		input = 0;
	}
	
	public double getOutput(){
		return output;
	}
	
	public double[] getCoords() {
		return coords;
	}
	
	//Just for network testing
	public double getDiff(int i) {
		return forwardConnections.get(i).weightDiff;
	}
	
	
	private class Connection {
		
		private double weight;
		private Node node;
		private double weightDiff;
    private double prevDiff;
    private double dEdw;
    private double prevdEdw;
		
		public Connection(Node n) {
			node = n;
			//Initial weights are random
			weight = 0.6*Math.random() - 0.3;
			weightDiff = 0;
      prevDiff = 0;
      dEdw = 0;
      prevdEdw = 0;
		}
		
		private void updateWeight(double a, double m) {
      weightDiff +=  m*prevDiff;
      /*/Quickprop
      double increase = (dEdw / (prevdEdw - dEdw))*prevDiff;
			if ((prevDiff >= 0 && weightDiff >= 0) || (prevDiff < 0 && weightDiff < 0))
        increase += a*weightDiff;
      if (increase >= 0) weight = Math.min(weight + increase, weight*1.75);
      else weight = Math.max(weight + increase, weight/1.75);/*/
      weight += a*weightDiff;
      
      prevDiff = weightDiff;
      prevdEdw = dEdw;
			weightDiff = 0;
      dEdw = 0;
		}
	}
}
